# Практика Avi ## Про `selectStatement` и `onRefreshExt` Изначально результат выборки формируется операцией `onRefresh` или `onRefreshItem` одним из двух способов: реляционный или объектный запрос. ### Реляционный запрос В этом случае в теле `onRefresh` вызывается метод `selectStatement` или `prepareSelectStatement` (вызывает тот же `selectStatement`, но с добавлением фильтров, описанных в `avm`). Иначе говоря, результатом операции `onRefresh` является текстовое значение, внутри которого реляционный запрос. ```{note} onRefreshExt в случае формирования выборки реляционным запросом не вызывается! ``` Так формируется по умолчанию отображение `List` (кроме коллекций). Не учитываются значения хранящиеся в кэше, учитываются значения только из БД. Пример `selectStatement` для отображения `List`: ```scala override protected def selectStatement: String = { s"""SELECT t.id ,t.idClass ,t.idState ,t1.sHeadLine_dz as idStateHL ,t.idStateMC ,t.idObjectType ,t2.sHeadLine_dz as idObjectTypeHL ,t.idDepOwner ,t3.sHeadLine_dz as idDepOwnerHL ,t.gidSrc ,t.idResourceHolder ,t4.sHeadLine_dz as idResourceHolderHL ,t.sRegNum ,t.dReg ,t.idPeriod ,t5.sHeadLine_dz as idPeriodHL ,t.sDescription ,t.gid ,t.sRegNum_dz ,t.sRegNumBMs_dz ,t.sRegNumidVer_dz FROM Oil_DemandMov t LEFT JOIN Btk_ClassState t1 on t.idState = t1.id LEFT JOIN Btk_ObjectType t2 on t.idObjectType = t2.id LEFT JOIN Bs_DepOwner t3 on t.idDepOwner = t3.id LEFT JOIN Bs_Contras t4 on t.idResourceHolder = t4.id LEFT JOIN Bs_Period t5 on t.idPeriod = t5.id """ } ``` ### Коммит в БД при выполнении реляционного запроса и @FlushBefore Важно понимать, что при реляционном запросе сервер приложения делает коммит в БД (особенность работы сервера приложения). Поэтому на выборках, где вводятся значения в поля и поддерживается возможность пользователю отменить изменения, нельзя использовать формирование выборки на реляционном запросе. Иначе данные запишутся в БД без разрешения пользователя (без нажатия пользователем на операцию сохранения (дискетка)). Однако, иногда есть необходимость в выполнении выборки на реляционном запросе в отображении с редактированием полей. Например, выборка выпадающего списка `Lookup`. В таком случае перед `onRefresh` необходимо добавить аннотоцию `@FlushBefore(mode = FlushBeforeMode.Disabled)`: ```scala trait Lookup extends Default with super.Lookup { @FlushBefore(mode = FlushBeforeMode.Disabled) override protected def onRefresh: Recs = { s"""SELECT t.id ,t.sHeadLine_dz as sHeadLine ,t.sMnemoCode_dz as sMnemoCode ,coalesce(t.sMnemoCode_dz, '') || ' ' || coalesce(t.sHeadLine_dz, '') as sMnemoCodeHeadLine from AsfEqp_IntoOperExt t order by upper(t.sHeadLine_dz) """ } } ``` Иначе при открытии выпадающего списка данные будут записываться в БД. ## Объектный запрос В этом случае onRefresh формируется из экземпляров объектов `SRop` (инструменты `refreshByParent`, `load`, `TxIndex`, `OQuery`) или `case class`’ов. В этом случае учитываются значения, хранящиеся в кэше. Примеры: По умолчанию отображение `List` у коллекций формируется с помощью `refreshByParent`: ```scala trait List_idPlanFactoryShip extends Default with super.List_Master { override protected def onRefresh: Recs = { Oil_PlanFactShipSegrGroupApi().refreshByParent(getIdMaster) } } ``` По умолчанию отображение `Card` у коллекций формируется с помощью `load`: ```scala trait Card extends Default with super.Card { override protected def onRefreshItem: Recs = { Oil_PlanFactShipSegrGroupApi().load(getVar(getPKFieldName).asNLong) } } ``` ### onRefreshExt В случае объектного запроса сервер сам вызывает метод `onRefreshExt`, в который можно дописать получение нехранимых полей на синтаксисе `SQL`, НЕ учитывая значения из кэша: ```scala override protected def onRefreshExt: String = { s"""with t as ( select :id as id ,:idPlanFactoryShip as idPlanFactoryShip ,:idSegregationGroup as idSegregationGroup ) SELECT t.id ,t1.sHeadLine_dz as idPlanFactoryShipHL ,t2.sHeadLine_dz as idSegregationGroupHL ,t2.sMnemoCode_dz as idSegregationGroupMC FROM t LEFT JOIN Oil_PlanFactoryShip t1 on t.idPlanFactoryShip = t1.id LEFT JOIN Oil_SegregationGroup t2 on t.idSegregationGroup = t2.id """ } ``` ```{note} Также существует другой способ реализовать нехранимые поля (через case class AdditionalInfo), который будет описан дальше. Данный способ формирует значения с учётом кэша. ``` ## Добавление нехранимых полей Здесь будет рассмотрено 2 случая, как реализовать нехранимые поля в выборке, реализованные реляционным запросом и объектным. Для формирования нехранимого поля нужно: - получить поле в запросе под заданным псевдонимом - описать поле в разметке `avm` под тем же псевдонимом ### Формировать реляционно или объектно Если нехранимый атрибут вычисляется динамически от изменений на выборке, это значит, что работа идёт с значениями из кэша, т.е. с значениями, которые ещё не закоммичены в БД. Это означает, что нехранимое поле реализовывать нужно на объектном запросе. Например: в карточке документа нужно посчитать сумму по всем записям в коллекции. Если поле суммы реализовать реляционно, то оно будет формироваться только из согласованных в БД значений. Это значит, для того чтобы учитывались новые записи в коллекции, их нужно закоммитить (сохранить) в БД. Если же поле реализовать объектно, то в нём будут учитываться данные из кэша, т.е. те самые созданные новые записи в коллекции, которые ещё не закоммичены в БД. ### Нехранимые поля в реляционном запросе ```{note} Не учитывает значения из кэша, которые не закоммичены в БД. ``` Чтобы добавить не хранимый атрибут реляционно, нужно иметь значение атрибута в выборке запроса `selectStatement`. Примером может служить результат кодогенератора ссылочного поля для отображения `List`, где описывается получение значения хэдлайна в `Dvi` и описание атрибута в `dvm`. Dvi: ```scala override protected def selectStatement: String = { s"""SELECT t.id ,t.idClass ,t.idObjectType ,t1.sHeadLine_dz as idObjectTypeHL --нехранимое поле ,t.dReg ,t.sRegNum ,t.idState ,t2.sHeadLine_dz as idStateHL --нехранимое поле FROM Rzd_Train t LEFT JOIN Btk_ObjectType t1 on t.idObjectType = t1.id LEFT JOIN Btk_ClassState t2 on t.idState = t2.id """ ``` dvm: ```xml ``` ```{note} Напомню, что Dvi и dvm - это результат работы кодогенератора по данным описанным в разметке odm. Разработчик работает в соответствующих файлах Avi и avm, переопределяя содержимое Dvi и dvm. ВНИМАНИЕ! Изменять Dvi и dvm нельзя. А точнее бесполезно, потому что изменения будут перетёрты при следующем запуске кодогенератора. ``` ### Нехранимые поля в объектном запросе ```{note} Учитывает значения из кэша, которые не закоммичены в БД. ``` #### onRefreshExt Данный метод вызывается, если результатом `onRefresh` является набор объектов, а не текст, представляющий реляционный запрос. С помощью `onRefreshExt` в отображении `Card` получены хэдлайны в `Dvi`: ```scala override protected def onRefreshExt: String = { s"""with t as ( select :id as id ,:idObjectType as idObjectType ,:idState as idState ,:gidSrc/*@NString*/ as gidSrc ) SELECT t.id ,t1.sHeadLine_dz as idObjectTypeHL ,t2.sHeadLine_dz as idStateHL ,t3.sHeadLine as gidSrcHL FROM t LEFT JOIN Btk_ObjectType t1 on t.idObjectType = t1.id LEFT JOIN Btk_ClassState t2 on t.idState = t2.id LEFT JOIN Btk_Object t3 on t.gidSrc = t3.gidRef """ } ``` Здесь используется синтаксис postgresql. Применяется конструкция with ([ссылка](https://www.postgresql.org/docs/current/queries-with.html)). Через `":"` подставляются текущие значения атрибутов с выборки (в том числе из кэша). Так, например, `":id as id"` означает, что с выборки будет получено текущее значение атрибута `id` и подставлено в таблицу `t` под псевдонимом поля `id`. Теперь обращение в основном запросе `t.id` будет возвращать текущее значеине `id`. ```{note} Под фразой "текущее значение" понимается значение на момент обновления выборки и выполнения метода onRefreshExt. ``` ```{note} Описание в dvm не меняется в зависимости от метода получения значения нехранимого поля (реляционного или объектного) и останется таким же, как в случае формирования нехранимого поля в selectStatement. ``` #### case class AdditionalInfo Нехранимое поле можно сформировать, переопределив `onRefresh` и `onRefreshItem`. В этом случае объект представляют как кортеж (`SRop`, экземпляр `case class’а`). Тем самым объект имеет поля записи, описанных в БД (хранимые поля), и поля `case class’а` (нехранимые). В этом случае принято называть `case class AdditionalInfo`, а заполнение описывать в методе `getAdditionalInfo`. Нехранимое поле в отображении `Card`: ```scala case class AdditionalInfo( var nQtyLoadRecievActs: NNumber, var nQtyLoadTransferCertificate: NNumber, var nQtyLoadTransferCertificateAccounted: NNumber, var nQtyRemains: NNumber, var nQtyReserv: NNumber ) protected def getAdditionalInfo(rop: Oil_ExternalMovApi#ApiRop): AdditionalInfo = { AdditionalInfo( nQtyLoadRecievActs = None.nn , nQtyLoadTransferCertificate = None.nn , nQtyLoadTransferCertificateAccounted = None.nn , nQtyRemains = None.nn , nQtyReserv = None.nn ) } override protected def onRefresh: Recs = { val rop = thisApi().load(getVar(CardRep.IdItemSharp).asNLong) (rop, getAdditionalInfo(rop)) } override protected def onRefreshItem: Recs = { val rop = thisApi().load(getVar(getPKFieldName).asNLong) (rop, getAdditionalInfo(rop)) } ``` ```{note} Переменные внутри case class необходимо указывать var для Avi ``` Нехранимое поле в отображении `List_idBrigade` коллекции: ```scala trait List_idBrigade extends Default with super.List_idBrigade { case class AdditionalInfo(var sEmpId: NString = None.ns, var sPosition: NString = None.ns) def getAdditionalInfo(rop: Bs_BrigadeStaffApi#ApiRop): AdditionalInfo = { if (rop.get(_.idEmployee).isNotNull) { val avEmploee = Bs_EmployeeApi().load(rop.get(_.idEmployee)).copyAro() AdditionalInfo( sEmpId = avEmploee.sEmpId, sPosition = avEmploee.sPosition) } else AdditionalInfo() } override protected def onRefresh: Recs = { Bs_BrigadeStaffApi().refreshByParent(getVarWithDep("super$id").asNLong) .map(rop => { (rop, getAdditionalInfo(rop)) }) } } ``` Такой способ самый универсальный. Например, с помощью `onRefreshExt` не получится посчитать сумму по всем записям коллекции с учётом кэша (для этого необходимо присоединить таблицу коллекции по условию `idparent = t.id`, что обеспечит учёт только тех записей и их значений, которые закоммичены в БД, т.е. без учёта кэша). В рассматриваемой реализации, можно получить все записи коллекции с учётом кэша с помощью метода `refreshByParent(rop.get(_.id))` и далее в обходчике сложить необходимые значения полей. ## Нехранимые строки Здесь будет описан пример реализации пустых предзаполненных строк в списке, как частный случай, в коллекции. Такие строки не хранятся в БД, а существуют визуально в интерфейсе (иными словами нехранимые строки). Запись в БД создаётся при вводе пользователем какого-либо поля. Пример в проекте pgDev: `ru.bitec.app.oil.Oil_RecievTaskDetAvi.List_idRecievTaskByFlyOverWay`. ![](img/avi-emptyRow.jpg) ### `case class Row` По сколку поля строки редактируемые, то не нужно использовать реляционный запрос для формирования выборки. Выборка будет формироваться экземплярами case class’а. Таким образом onRefresh вернёт список экземпляров case class’а. В примере в коллекции «Подача» имеет столько строк, сколько вагонов указано в записи «Путь эстакады», на который ссылается мастер-документ. Case class должен иметь такие же поля, как поля класса (таблицы), описанные в odm и нехранимые, формируемые не реляционно. При добавлении нового хранимого поля, необходимо это поле добавить в case class. ```scala /** * Представление строки, поля соответствуют хранимым полям строки * * Полем id выступает idFlyOverPos, т.к. для работы некоторых инструментов (например, onRefreshExt) необходимо * наличие уникального индификатора */ case class Row( var id: NLong //Oil_RecievTaskDet.idFlyOverPos , var gid: NGid = None.ng , var idRecievTask: NLong , var nRow: NNumber , var idWagon: NLong = None.nl , var idRailwayInvoice: NLong = None.nl , var bUnloaded: NNumber = None.nn , var bReFeed: NNumber = None.nn , var idReFeedType: NLong = None.nl , var bWagonRemoved: NNumber = None.nn , var bRequiredMeasure: NNumber = None.nn , var nQtyMeasure: NNumber = None.nn , var idStorageTank: NLong = None.nl , var sCertificateList: NString = None.ns , var idRecievAct: NLong = None.nl , var bRepeatedFeed: NNumber = None.nn , var bCommAct: NNumber = None.nn , var bRecievActCreated: NNumber = None.nn , var idFlyOverPos: NLong = None.nl , var idLockDeviceOut: NLong = None.nl , var idRecievTaskDet: NLong = None.nl //Oil_RecievTaskDet.id , var idTrain: NLong = None.nl , var bRecievByMeasure: NNumber = None.nn ) ``` ### `onRefresh` `ru.bitec.app.oil.Oil_RecievTaskDetAvi.List_idRecievTaskByFlyOverWay#onRefresh`. Описание алгоритма: - Получаем все записи данной коллекции по мастеру (т.е. те, что хранятся в БД или в кэше). ```scala RecievTaskDetApi().refreshByParent(getIdMaster) ``` - Получаем все вагоны выбранной у мастера «Пути эстакады». ```scala val ropParent = Oil_RecievTaskApi().load(selection.master.getSelfVar("id").asNLong) Oil_FlyOverPosApi().txidFlyOverWay.refreshByKey(ropParent.get(_.idFlyOverWay)) ``` ```{note} В примере в коллекции «Подача» имеет столько строк, сколько вагонов указано в записи «Путь эстакады», на который ссылается мастер-документ. `````` - Реализуем обходчик по каждому вагону и заполняем экземпляры case class’а - Если на данный вагон есть запись из таблицы БД, то заполняем данными из БД - Иначе устанавливаем предзаполненные значения необходимых полей или `None` ```scala ropaFOP.map(ropFOP => { //поиск ропы в БД val ropOpt = ropa.find(_.get(_.idFlyOverPos) === ropFOP.get(_.id)) getRowByRop(ropOpt, ropFOP, nvRow) }) protected def getRowByRop(ropOpt: Option[SRop[_ <: JLong, _ <: Oil_RecievTaskDetAro]] , ropFOP: Oil_FlyOverPosApi#ApiRop , npRow: NNumber = None.nn ): Row = { Row( idPlacement = ropOpt.map(_.get(_.idPlacement)).nl .nvl(thisApi().getidPlacementByFlyOverPos(ropFOP.get(_.id))) ...) } ``` При добавлении нового хранимого атрибута, необходимо в этом методе заполнения описать правило заполнения нового поля. На данном этапе предположим, что `id` заполняется от `ropOpt` (реальной записи), в случае отсутствия `None.nl`. Т.е. пустая строка имеет `id = None.nl`, что дальше будет использоваться, как признак пустой строки. ```{note} Внимание: такая реализация не совсем корректна. В таком случае не будет работать ряд системных операций, таких, как onRefreshExt, которые требуют уникального значения id. Но для понимания будет рассмотрена такая реализация, а вопрос уникальности id будет рассмотрен далее. ``` `Row – case class`, представление строки, поля соответствуют хранимым полям записи. - Возвращаем полученный список экземпляров case class’а. ### `insert` при вводе значения в нехранимую строку Необходимо переопределить операцию `beforeEdit()`. ```scala override def beforeEdit(): Unit = { if (getSelfVar("id").isNull) { regRow() } } ``` `beforeEdit()` вызывается при каждой попытке редактировать поле. Здесь необходимо инициализировать, что заполнение введётся в пустой нехранимой строке или в хранимой. Это можно сделать, проверив заполнен ли `id` (на данном этапе предполагается, что у пустой строки `id = None.nl`). Если заполнение ведётся в пустой строке, то необходимо создать запись с переносом всех предустановленных значений и введённое пользователем значение применить к только что созданной записи. Переносить предустановленные значения можно двумя способами: - Прописать, какой сеттер вызывать и какое значение из case class’а подставлять на каждое поле в отдельности. Такая реализация усложняет поддержание кода. При добавлении нового хранимого атрибута, в случае если оно имеет предустановленное значение, нужно добавлять код в этот фрагмент. - Реализовать обходчик по всем имеющимся полям case class’a, отсечь поля, которые не имеют сеттеров (`gid`, например) и значение которых `null`, по оставшимся полям вызывать сеттеры и проставлять соответствующие значения: ```scala /** * insertByParent(ropParent) + сеттеры заполненных полей case class'а Row * * @param ropParent * @param row * @return */ def insertByParentAndRow(ropParent: Oil_RecievTaskApi#ApiRop, row: Row): ApiRop = { insertByParent(ropParent) :/ { rop => //setter'ы для переноса умолчательных значений из Row row.getClass.getDeclaredFields.map(_.getName.ns).zip(row.productIterator.to) .filterNot(field => saFieldsNotSetter.contains(field._1) || field._2.asInstanceOf[Nullable[_ <: Any, _]].isNull) .foreach(field => { setAttrValue(rop, field._1, field._2) }) rop } } ``` Чтобы введенное пользователем значение относилось к только что созданной записи, необходимо принудительно после регистрации задать `id` на выборке: ```scala val rop = thisApi().registerByRow(thisRow()) setVar("id", rop.get(_.id)) ``` ### `afterEdit()` Также необходимо добавить проверку, является ли строка пустой, в `afterEdit()`. Иначе `load()` внутри `afterEdit()` будет вызван по некорректного id пустой строки и будет вызвана ошибка. ### Поддержание уникального id в нехранимых полях. Без уникального `id` ряд системных операций не будет работать или будет работать некорректно, в том числе для работы `onRefreshExt`. Необходимо понять, что на выборке является уникальным, в случае примера это ссылка соответствующая вагону: `idFlyOverPos` . Теперь параметр `id` на выборке будет иметь не значение `null` в случае пустой строки и значение записи коллекции из БД, а значение `idFlyOverPos`. А сам `id` записи коллекции будет храниться в поле case class’a `idRecievTaskDet` Это нужно учесть в: - Структуре case class’а - В переносе значений из записи из БД в case class для формирования `onRefresh` - В условии по признаку пустая ли строка (теперь строка пустая, если `idRecievTaskDet === None.nl`): ```scala override def beforeEdit(): Unit = { if (getSelfVar("idRecievTaskDet").isNull) { regRow() } } ``` - При сеттере в поле пустой строки в методе переноса умолчательных значений: ```scala /** * insertByParent(ropParent) + сеттеры заполненных полей case class'а Row * * @param ropParent * @param row * @return */ def insertByParentAndRow(ropParent: Oil_RecievTaskApi#ApiRop, row: Row): ApiRop = { insertByParent(ropParent) :/ { rop => //setter'ы для переноса умолчательных значений из Row row.getClass.getDeclaredFields.map(_.getName.ns).zip(row.productIterator.to(scala.collection.immutable.IndexedSeq)) .filterNot(field => saFieldsNotSetter.contains(field._1) || field._2.asInstanceOf[Nullable[_ <: Any, _]].isNull) .foreach(field => { if (field._1 == sid.ns) { setAttrValue(rop, sidFlyOverPos, field._2) } else { setAttrValue(rop, field._1, field._2) } }) rop } } ``` - При сеттере в поле пустой строки после создания записи проставить параметр выборки `idRecievTaskDet`, заместо `id`, в `id` только что созданной записи: ```scala val rop = thisApi().registerByRow(thisRow()) setVar("idRecievTaskDet", rop.get(_.id)) ``` - При взятии параметра у дочерних выборок по `super$id` -> `super$idRecievTaskDet` - В `CWA` управление свойством `isEnabled` у операций: ```scala selection.opers().setEnabled("Delete",selection.canDelete && selection.getSelfVar("idRecievTaskDet").notNull()) ``` - Удаление: ```scala thisApi().delete(thisApi().load(getSelfVar("idRecievTaskDet").asNLong)) ``` - thisRop() ```scala thisApi().load(getSelfVar("idRecievTaskDet").asJLong) ``` - `onInvalidateItem()`: ```scala override protected def onInvalidateItem(): Unit = { if (!getSelfVar(thisApi().sidRecievTaskDet).isNull && thisRop() != null) { session.invalidateObject(thisRop()) } } ``` - учесть в `onRefreshExt()` в тексте запроса ## Динамическое присоединение столбцов Если такое отображение только для чтения и результат не использует значения, которые могут быть в кэше, тогда можно реализовать на реляционном запросе, иначе необходимо использовать инструменты `DynMetaBuilder` и `DynRecBuilder`. ### Реляционная реализация В этом случае необходимо собрать текст запроса `selectStatement`. Ниже пример формирования одного из столбца: ```scala s""" ,(select string_agg(cast(t.id as varchar), ', ') from Oil_Task t where t.idResource = ${rv.idResource()} and (t.dBegin < $sMainFromTab.dEndTime and t.dExec > $sMainFromTab.dBegTime) and t.idObjectType = :${Oil_TaskMonitorPkg().sfltidObjectType} and t.idStateMC >= 200 and t.idDepOwner = :super$$idGlobalDepOwner group by t.idResource ) as \"$sNameFieldForGST[${rv.id()}]\" """ ``` Пример текста после подстановки значения: ```{code} (select string_agg(cast(t.id as varchar), ', ') from Oil_Task t where t.idResource = 115 and (t.dBegin < ‘2023-08-25 12:00:00’ and t.dExec > ‘2023-08-25 14:00:00’) and t.idObjectType = 225 and t.idStateMC >= 200 and t.idDepOwner = 336 group by t.idResource ) as “idFlyOverWay[13]" ``` Мы получим столбец с названием `idFlyOverWay[13]`, результатом будет значение подзапроса. ### `DynMetaBuilder` и `DynRecBuilder` Пример: `ru.bitec.app.oil.Oil_TaskMonitorAvi.List_CoreResource`. Структура формирования: ```scala Recs(<объекты, представляющие строку (список rop или case class)>) .extend(.build()) .foreach((row, builder) => (row, .build)) ``` - Пример использования `Recs`: ```scala Recs(SomeApi().refreshByParent(ropMaster)) ``` ,где SomeApi() - какая-либо Api класса. - Пример использования `.extend()` Здесь формируются методанные о добавляемых столбцах: название столбца, тип данных, caption. ```scala val dynMetaBuilder = DynMetaBuilder() dynMetaBuilder.add("idFlyOverWay[1]", classOf[String], "ПЭ-1") dynMetaBuilder.add("idFlyOverWay[20]", classOf[String], "ПЭ-2") dynMetaBuilder.add("idFlyOverWay[360]", classOf[String], "ПЭ-3") Recs(SomeApi().refreshByParent(ropMaster)) .extend(dynMetaBuilder.build()) ``` ,где SomeApi() - какая-либо Api класса. - Пример использования .foreach() Стоит понимать, что это не привычный обходчик по коллекции, который ничего не возвращает, а отдельный метод для динамического формирования столбцов, который должен ВЕРНУТЬ результат выборки! В данном методе столбец получает построчно значения. ```scala Recs(SomeApi().refreshByParent(ropMaster)) .extend(dynMetaBuilder.build()) .foreach((row, builder) => { builder.set(("idFlyOverWay[1]", row.get(_.sDiscription))) builder.set(("idFlyOverWay[20]", "hello, world")) builder.set(("idFlyOverWay[20]", None.ns)) //метод должен вернуть кортеж (row, builder.build) }) ``` ,где SomeApi() - какая-либо Api класса. Тут заданы различные значения для столбцов. На каждую строку `row` будет формироваться 3 значения для 3ёх столбцов. Столбец `idFlyOverWay[1]` будет в каждой строке разный, остальные же иметь одинаковое значение. Описать столбец в разметке `avm` можно по имени поля без квадратных скобок. ## Динамическое изменение свойств avm Для этого необходимо использовать следующий метод: `ru.bitec.app.gtk.gl.Rep#setMetaProp` [Описание](https://docs.global-system.ru/as/dev/reference/api/core-gtk/ru/bitec/gtk/core/gl/avi/CoreRepProxy.html#setmetaprop) Пример использования: ```scala setMetaProp(attr.sSystemName.get //для какого поля меняется свойство //какое свойство //в данном случае свойство задает системное имя атрибута в выборке, //которое хранит настройки типа редактора , s"View.Representation.Attributes.Attribute.Editor.editorTypeAttr"меняется , thisPkg.getSEditorAttrName(attr) //значения свойства ) ``` ## `OnrefreshExt` и значение даты с выборки В `onRefreshExt` нет приведения `/*NDate*/`, нужно писать через `/*NString*/`. В `onRefreshExt` в конструкции with объявляются поля, которые нужно получить с выборки (из кэша). Иногда требуется указать тип данных. Тип данных указывается в `/*...*/`, но парсер на знает `NDate`, поэтому необходимо указать, как `NString` и далее использовать в запросе `cast(... as timestamp)` или `as date`. ```scala override protected def onRefreshExt: String = { s"""with t as ( select :id as id ,:idState as idState ,:gidSrc/*@NString*/ as gidSrc ,:idObjectType as idObjectType ,:idDepOwner as idDepOwner ,:idDischargePlace as idDischargePlace ,:idSegregationGroup as idSegregationGroup ,:idService as idService ,:idFlyOverWay as idFlyOverWay ,:idStateMC as idStateMC ,:dPlanEnd /*@NString*/ as dPlanEnd ,:dPlanBegin /*@NString*/ as dPlanBegin ) SELECT t.id ,to_char(cast(t.dPlanEnd as timestamp) - cast(t.dPlanBegin as timestamp), 'dd д. hh24 ч.') as nPlanBusy ,t1.sHeadLine_dz as idStateHL ,t2.sHeadLine_dz as idObjectTypeHL ,t3.sHeadLine_dz as idDepOwnerHL ,t4.sHeadLine_dz as idDischargePlaceHL ,t5.sMnemoCode_dz as idSegregationGroupHL ,t6.sHeadLine_dz as idServiceHL ,t7.sHeadLine as gidSrcHL ,t8.sHeadLine_dz as idFlyOverWayHL FROM t LEFT JOIN Btk_ClassState t1 on t.idState = t1.id LEFT JOIN Btk_ObjectType t2 on t.idObjectType = t2.id LEFT JOIN Bs_DepOwner t3 on t.idDepOwner = t3.id LEFT JOIN Bs_Placement t4 on t.idDischargePlace = t4.id LEFT JOIN Oil_SegregationGroup t5 on t.idSegregationGroup = t5.id LEFT JOIN Gds_Service t6 on t.idService = t6.id LEFT JOIN Btk_Object t7 on t.gidSrc = t7.gidRef LEFT JOIN Oil_FlyOverWay t8 on t.idFlyOverWay = t8.id """ } ``` ## Поиск отображения на выборке `selection.form.findSelection(...)` [Ссылка](https://docs.global-system.ru/as/dev/reference/api/core-gtk/ru/bitec/gtk/core/gl/CoreForm.html#findselection) Полезный метод, который позволяет найти отображение по системному имени среди видимых в сессии. Пример использования: ```scala val sel = selection.form.findSelection(Bdg_ForecastAvi.card()) ``` ## Пользовательская блокировка Пользовательская блокировка включается при взаимодействии пользователя с интерфейсом в методе `Dvi#beforeEdit`, который является результатом кодогенератора. Принудительно можно включать блокировку с помощью вызова метода: - `Btk_FormSessionApi().lockObject(gid)` - `Btk_FormSessionApi().lockObjectMulti(gida)` Больше про пользовательскую блокировку можно узнать [здесь](http://helpcenter.gs.local/btk_documentaion_v1/html/120_%D0%9F%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B0%D1%8F%20%D0%B1%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0.html). ## Объект класса в процессе создания и другие состояния объекта rop Иногда, есть необходимость в Avi знать, что запись находится в процессе создания, т.е. не имеет реализации, как запись в таблице БД. Это можно узнать по объекту записи `rop`: - используйте метод Avi `thisRop()` чтобы получить объект `rop`, чья карточка открыта или на котором стоит фокус, если отображение `list`. - если `rop.ropMode == InsertRopMode`, то объект находится в процессе создания, т.е. есть в кэше приложения, но не имеет реализации в таблице БД. Где поле объекта `rop.ropMode` хранит в себе информацию состояния объекта. Есть и иные состояния объекта: - ReadRopMode - UpdateRopMode - DeleteRopMode - InsertRopMode Пример кода из проекта: ```scala //Документ должен быть закоммичен, чтобы были пройдены все соответсвующие проверки, // т.к. по закрытию card_ReadFromFile будет flush() if (rop.ropMode == InsertRopMode) { throw AppException("Для вызова операции необходимо, чтобы документ был заполенен и сохранён.") } ``` ## Как узнать, что выборка является главным меню или главной выборкой формы В системе есть 3 типа форм: - главная - модальная - MDI (отображается в качестве закладки на главной форме) У каждой формы есть главная выборка. Флаг `selection.isMainOnForm` и указывает, что выборка - главная на форме. Условие `application.mainSelection == selection` определит, является ли выборка главным меню.